/*
 * This file is a part of ShtutXML
 * © Barak Itkin <lightningismyname at gmail dot com>, 2011
 *
 * ShtutXML is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.shtutxml;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A class to represent a structured XML document, in the form that is needed by
 * a WYSWYG editor. What it has is a list of strings, each one associated with a
 * {@link Node} which is it's direct parent.
 */
public class StrXML {

    /**
     * A class to represent a location inside the an StrXML object
     */
    public class NodeAndOffset {

        /**
         * Offset of the text when counting text only directly inside this node
         */
        final public int TextOffsetInsideNode;
        /**
         * Offset of the text when counting all the text in the document
         */
        final public int TextGlobalOffset;
        /**
         * Offset of the text since the begining of this section in the StrXML
         * document
         */
        final public int TextSectionOffset;
        final public TextSection textSection;

        NodeAndOffset(int TextOffsetInsideNode, int TextGlobalOffset, int TextSectionOffset, TextSection textSection) {
            this.TextOffsetInsideNode = TextOffsetInsideNode;
            this.TextGlobalOffset = TextGlobalOffset;
            this.TextSectionOffset = TextSectionOffset;
            this.textSection = textSection;
        }

        public String getTextContent() throws DOMException {
            return textSection.getTextContent();
        }

        public Node getParentNode() {
            return textSection.getParentNode();
        }

        public Node getTextNode() {
            return textSection.textNode;
        }

        @Override
        public String toString() {
            return getParentNode() + " at " + TextOffsetInsideNode;
        }

        public StrXML getStrXML() {
            return StrXML.this;
        }
    }
    TextSection firstSection, lastSection;
    Document XMLDocument;

    protected StrXML(Document doc) {
        this.XMLDocument = doc;
        this.firstSection = this.lastSection = null;
    }

    /**
     * When constructing
     * @param str
     * @param node
     */
    void add(String str, Node node) {
        TextSection newOne = new TextSection(this.lastSection, node);
        if (this.firstSection == null) /* The the last is also null */ {
            this.firstSection = this.lastSection = newOne;
        } else {
            this.lastSection.InsertAfterMe(newOne);
            this.lastSection = this.lastSection.next;
        }
    }

    public NodeAndOffset GetOffset(int GlobalOffset) {
        int SectionOffset = GlobalOffset;
        int InNodeOffset = 0;
        TextSection where = null;

        for (TextSection section = this.firstSection; section != null; section = section.next) {
            if (SectionOffset < section.getTextContent().length()) {
                where = section;
                break;
            } else {
                SectionOffset -= section.getTextContent().length();
            }
        }

        for (TextSection section = this.firstSection; section != null; section = section.next) {
            if (section == where) {
                break;
            } else if (section.getParentNode() == where.getParentNode() || section.getParentNode().isSameNode(where.getParentNode())) {
                InNodeOffset += section.getTextContent().length();
            }
        }

        return new NodeAndOffset(InNodeOffset + SectionOffset, GlobalOffset, SectionOffset, where);
    }

    public String debug() {
        StringBuilder str = new StringBuilder();
        for (TextSection section = this.firstSection; section != null; section = section.next) {
            str.append(section.getParentNode());
            str.append(": ");
            str.append(section.getTextContent());
            str.append("\n");
        }
        return str.toString();
    }

    /**
     * Return all the text of the given XML document, without any XML tags or
     * other stuff. Just the text content.
     * @return A string with the XML's text content
     */
    public String GetText() {
        StringBuilder strbuf = new StringBuilder();
        for (TextSection section = this.firstSection; section != null; section = section.next) {
            strbuf.append(section.getTextContent());
        }
        return strbuf.toString();
    }

    TextSection splitSection(NodeAndOffset nao) {
        TextSection next = nao.textSection.splitSection(nao.TextSectionOffset);
        if (nao.textSection == lastSection) {
            lastSection = next;
        }
        return next;
    }

    TextSection splitSectionUpTo(NodeAndOffset nao, Node lastSplitBeforHere) {
        TextSection next = nao.textSection.splitSectionUp(nao.TextSectionOffset, lastSplitBeforHere);
        if (nao.textSection == lastSection) {
            lastSection = next;
        }
        return next;
    }

    TextSection splitSectionUpToNotIncluding(NodeAndOffset nao, Node lastSplitOnChild) {
        TextSection next = nao.textSection.splitSectionUpNotIncluding(nao.TextSectionOffset, lastSplitOnChild);
        if (nao.textSection == lastSection) {
            lastSection = next;
        }
        return next;
    }

    public static StrXML parseDocument(Document doc) {
        final StrXML result = new StrXML(doc);
        new DepthDocumentVisitor() {

            @Override
            protected void visitText(Node n, int depth) {
                result.add(n.getTextContent(), n);
            }
        }.visit(doc, DepthDocumentVisitor.VisitOrder.DEPTH_FIRST);
        return result;
    }

    public void insertAndSplitInserted(Node n, StrXML.NodeAndOffset begin, StrXML.NodeAndOffset end) {
        if (end.getStrXML() != this || begin.getStrXML() != this) {
            throw new IllegalArgumentException("Position from bad StrXML's!");
        }

        TextSection S = begin.textSection, E = end.textSection, newT, newTT;
        int BeginOff = begin.TextOffsetInsideNode;
        int EndOff = end.TextOffsetInsideNode;

        if (S == E) {
            newT = S.splitSection(BeginOff);
            newTT = newT.splitSection(EndOff - BeginOff);
            newT.wrapWith(n.cloneNode(false));
            if (E == lastSection) {
                lastSection = newTT;
            }
        } else {
            newT = S.splitSection(BeginOff);
            newT.wrapWith(n);
            S = newT.next;
            while (S != E) {
                S.wrapWith(n.cloneNode(false));
                S = S.next;
            }
            newT = S.splitSection(EndOff);
            S.wrapWith(n.cloneNode(false));
            if (S == lastSection) {
                lastSection = newT;
            }
        }
    }

    public void insertAndSplitParent(Node n, StrXML.NodeAndOffset begin, StrXML.NodeAndOffset end) {
        if (end.getStrXML() != this || begin.getStrXML() != this) {
            throw new IllegalArgumentException("Position from bad StrXML's!");
        }

        TextSection S = begin.textSection, E = end.textSection, newT, newTT;
        int BeginOff = begin.TextOffsetInsideNode;
        int EndOff = end.TextOffsetInsideNode;

        Node LCA = Manipulator.LowestCommonAncesstor (S.getParentNode(), E.getParentNode());

        TextSection first = splitSectionUpToNotIncluding (begin, LCA);
        end = GetOffset(end.TextGlobalOffset);
        TextSection AfterLast = splitSectionUpToNotIncluding (end, LCA);

        NodeList children = LCA.getChildNodes();
        boolean foundFirst = false;
        boolean foundAfterLast = false;

        Node child = null;

        for (int i = 0; i < children.getLength(); i++) {
            child = children.item(i);
            if (!foundFirst)
                foundFirst = TextSection.isChildOrSame (child, first.textNode);
            else
                foundAfterLast = TextSection.isChildOrSame (child, AfterLast.textNode);

            if (foundAfterLast)
                break;
            else if (foundFirst) {
                n.appendChild (child);
                /* We removed a child, so we must refresh the children list */
                children = LCA.getChildNodes();
                i --;
            }
        }

        if (foundAfterLast)
            LCA.insertBefore(n, child);
        else
            LCA.appendChild(n);

        if (E == lastSection)
            lastSection = AfterLast;
    }
}
